# -*- coding: utf-8 -*-
"""
UTc! module: src.lobby

Purpose
=======
 Implements the server-side game interface.
 
Legal
=====
 All code, unless otherwise indicated, is original, and subject to the
 terms of the GPLv3, which is provided in COPYING.
 
 (C) Neil Tallim, 2009
"""
import socket
import threading
import time
import re

import communication
import data_box

_VERSION = "1.1.2d" #: The version of UTc! with which this server is compatible.
_TEAM_SIZE = 16 #: The maximum number of players that may play on a single team.

_TEAM_REGEXP = re.compile(r"(TEAM) \w+ (\d+)") #: A protocol-regexp that catches team-change events.
_PART_REGEXP = re.compile(r"(PART) \w+") #: A protocol-regexp that catches player-part events.
_PLAY_REGEXP = re.compile(r"(PLAY) \w+ (\d+),(\d+),(\d+) (\d{2}) (\d{2}|__),(\d{2}|__),(\d{2}|__),(\d{2}|__) (\d),(\d),(\d),(\d) (\d{2}|__),(\d{2}|__),(\d{2}|__) (\d)") #: A protocol-regexp that catches player-game-join events.
_TEXT_REGEXP = re.compile(r"TEXT \w+ .+") #: A protocol-regexp that catches player-text events.
_REGEXP_HANDLING = (
 (_TEAM_REGEXP, True),
 (_PART_REGEXP, True),
 (_PLAY_REGEXP, True),
 (_TEXT_REGEXP, False)
) #: In order of expected frequency. A value of True means that the server needs to process the line instead of just forwarding it.

class Lobby(threading.Thread):
	"""
	A class for managing the lobby component of a UTc! server.
	"""
	_alive = True #: False when the server has died.
	_client_lock = None #: A lock used to facilitate communication between clients and lobby-space.
	_game_port = None #: Non-None when a game is in progress.
	_play_callback = None #: The function used to add a player to a game.
	_players = None #: A map of all players that are connected to the lobby.
	_player_client_map = None #: The mapping between player names and connections.
	_server = None #: The network server associated with this lobby.
	_teams = None #: A collection of all teams.
	_validate_callback = None #: A function that may be used to determine whether the player's name is in use.
	
	def __init__(self, port, play_callback, validate_callback):
		"""
		Sets up and starts the lobby server.
		
		@type port: int
		@param port: The port on which the lobby server will run.
		@type play_callback: callable
		@param play_callback: The function to call when a client wants to join an
		    in-progress game.
		@type validate_callback: callable
		@param validate_callback: The function to call when deciding whether a
		    player's name is already in use.
		
		@raise socket.error: If the specified port is already in use.
		"""
		threading.Thread.__init__(self)
		self.setDaemon(False)
		
		self._client_lock = threading.RLock()
		self._player_client_map = {}
		self._players = {}
		self._teams = tuple([_Team() for i in range(6)])
		
		self._play_callback = play_callback
		self._validate_callback = validate_callback
		
		self._server = LobbyServer(port, self)
		self._server.start()
		
	def addPlayer(self, player_name):
		"""
		Adds a player to the lobby, creating an _Player object to maintain a local
		representation of its state.
		
		@type player_name: basestring
		@param player_name: The name of the player to be added.
		
		@rtype: L{_Player}
		@return: The newly created representative player object.
		"""
		self._client_lock.acquire()
		try:
			target_team = self._teams[0]
			target_team_size = len(target_team.getMembers())
			target_team_id = 0
			for (i, team) in enumerate(self._teams[1:]):
				team_size = len(team.getMembers())
				if team_size < target_team_size:
					target_team = team
					target_team_size = team_size
					target_team_id = i + 1
					
			player = _Player(player_name, target_team_id)
			self._players[player_name] = player
			target_team.addMember(player)
			
			self._pushEvent("JOIN %s %i" % (player_name, target_team_id + 1))
			
			return player
		finally:
			self._client_lock.release()
			
	def connectPlayer(self, player, connection):
		"""
		Handles the process of completing a connection from a client to the lobby
		server.
		
		@type player: L{_Player}
		@param player: The player being connected.
		@type connection: L{ClientConnection}
		@param connection: The connection thread that manages communication with
		    this player.
		"""
		self._client_lock.acquire()
		try:
			player_name = player.getName()
			self._player_client_map[player_name] = connection
			if self._game_port:
				self._pushEvent("GSTR", player_name, False)
		finally:
			self._client_lock.release()
			
	def disconnectPlayer(self, player_name):
		"""
		Removes a player from this lobby server.
		
		@type player_name: basestring
		@param player_name: The name of the player who is being removed.
		"""
		self._client_lock.acquire()
		try:
			if self._dropPlayer(player_name):
				self._pushEvent("PART %s" % (player_name))
		finally:
			self._client_lock.release()
			
	def endGame(self, teams, players):
		"""
		Called to inform all clients that the game server has shut down.
		
		All team stats and player stats are published by this function.
		
		@type teams: sequence
		@param teams: A collection of L{game._Team} objects.
		@type players: sequence
		@param players: A collection of L{game._Player} objects.
		"""
		self._game_port = None
		self._pushEvent("GEND")
		for (i, team) in enumerate(teams):
			self._pushEvent("TSTT %i %i %i" % (i + 1, team.getRank(), team.getScore()))
		for player in reversed(sorted(players)):
			(accuracy, kills, deaths, suicides) = player.getStats()
			self._pushEvent("PSTT %s %i %i %i %i %s" % (player.getName(), player.getTeamID() + 1, kills, deaths, suicides, accuracy))
			
	def getPlayers(self):
		"""
		Returns a collection of all players who are connected to this lobby
		server.
		
		@rtype: sequence
		@return: A collection of L{_Player} objects.
		"""
		self._client_lock.acquire()
		try:
			return self._players.values()
		finally:
			self._client_lock.release()
			
	def getTeam(self, id):
		"""
		Returns a team-status representative container.
		
		@type id: int
		@param id: The ID of the team to retrieve.
		
		@rtype: L{_Team}
		@return: The team being requested.
		"""
		return self._teams[id]
		
	def hasPlayer(self, name):
		"""
		Indicates whether this lobby server has a player with the given name.
		
		@type name: basestring
		@param name: The name of the user being sought.
		
		@rtype: bool
		@return: True if the named user is present.
		"""
		self._client_lock.acquire()
		try:
			return name in self._players
		finally:
			self._client_lock.release()
			
	def isAlive(self):
		"""
		Indicates whether this lobby server is active.
		
		@rtype: bool
		@return: True if this lobby server is still active.
		"""
		return self._alive
		
	def kill(self):
		"""
		Indicates that this lobby server should terminate gracefully.
		"""
		self._alive = False
		
	def processLine(self, line, player_name):
		"""
		Interprets data from a client, applying protocol regexps to determine how
		to react.
		
		@type line: basestring
		@param line: The line to process.
		@type player_name: basestring
		@param player_name: The name of the player who generated this line.
		"""
		for (regexp, process) in _REGEXP_HANDLING:
			match = regexp.match(line)
			if match:
				if not process: #Just forward it.
					self._pushEvent(line, player_name, True)
					break
				else: #Determine the nature of the event and deal with it.
					nature = match.group(1)
					self._client_lock.acquire()
					try:
						if nature == "TEAM":
							player = self._players.get(player_name)
							if player:
								team_id = int(match.group(2)) - 1
								if team_id < len(self._teams):
									team = self._teams[team_id]
									if len(team.getMembers()) < _TEAM_SIZE:
										self._teams[player.getTeamID()].removeMember(player)
										self._teams[team_id].addMember(player) 
										player.setTeamID(team_id)
										self._pushEvent("TEAM %s %i" % (player_name, team_id + 1))
										return
								self._pushEvent("TEAM %s %i" % (player_name, player.getTeamID() + 1), player_name, False)
						elif nature == "PART":
							for (name, connection) in self._player_client_map.iteritems():
								if name == player_name:
									connection.kill()
									return
							self._pushEvent("PART %s" % (player_name), player_name, True)
						elif nature == "PLAY":
							player = self._players.get(player_name)
							if player:
								if self._game_port:
									join_result = self._play_callback(
									 player_name, player.getTeamID(),
									 (float(match.group(2)) / 1000.0, float(match.group(3)) / 1000.0, float(match.group(4)) / 1000.0),
									 match.group(5),
									 (match.group(6), match.group(7), match.group(8), match.group(9)),
									 (int(match.group(10)), int(match.group(11)), int(match.group(12)), int(match.group(13))),
									 (match.group(14), match.group(15), match.group(16)),
									 int(match.group(17))
									)
									if not join_result:
										self._pushEvent("GAME %i" % (self._game_port), player_name, False)
									else:
										self._pushEvent("NOGM %s" % (join_result), player_name, False)	
								else:
									self._pushEvent("NOGM no game in progress", player_name, False)
					finally:
						self._client_lock.release()
					break
					
	def run(self):
		"""
		Keeps this lobby server running as long as the host maintains the game
		process.
		"""
		while self._alive:
			time.sleep(10.0)
			
		self._server.kill()
		self._pushEvent("SEND")
		for connection in self._player_client_map.values():
			connection.kill()
			
	def startGame(self, port):
		"""
		Informs all clients that a game server has started.
		"""
		self._game_port = port
		self._pushEvent("GSTR")
		
	def validatePlayer(self, player_name):
		"""
		Determines whether the named player is known to either the lobby server
		or the game server, if any.
		
		@type player_name: basestring
		@param player_name: The name of the player being sought.
		
		@rtype: str|None
		@return: A string indicating where the player was found or None if the
		    player was not found.
		"""
		if self.hasPlayer(player_name):
			return "player already in lobby"
		elif self._validate_callback(player_name):
			return "player currently in game"
		return None
		
	def _dropPlayer(self, player_name):
		"""
		Performs the practical process of removing a player from this lobby
		server, if the player was actually present to begin with.
		
		@type player_name: basestring
		@param player_name: The name of the player to be removed.
		
		@rtype: bool
		@return: True if the player was removed; False if the player was not
		    found.
		"""
		self._client_lock.acquire()
		try:
			player = self._players.get(player_name)
			if player:
				del self._players[player_name]
				self._teams[player.getTeamID()].removeMember(player)
				return True
			return False
		finally:
			self._client_lock.release()
			
	def _pushEvent(self, event, player_filter=None, exclude_named_player=False):
		"""
		Propagates an event to a subset of all connected clients.
		
		@type event: basestring
		@param event: The event to send.
		@type player_filter: basestring|None
		@param player_filter: The name of the player to treat specially, or None
		    if all players should receive this message.
		@type exclude_named_player: bool
		@param exclude_named_player: True if the named player should not receive
		    this event; False if B{only} the named player should receive it.
		"""
		self._client_lock.acquire()
		try:
			if not player_filter:
				for (name, connection) in self._player_client_map.iteritems():
					connection.pushEvent(event)
			else:
				for (name, connection) in self._player_client_map.iteritems():
					if exclude_named_player:
						if name == player_filter:
							continue
						connection.pushEvent(event)
					elif not exclude_named_player and name == player_filter:
						connection.pushEvent(event)
						break
		finally:
			self._client_lock.release()
			
			
class LobbyServer(communication.ServerSocket):
	"""
	A server process that continually listens for and accepts connections from
	would-be clients.
	"""
	def __init__(self, port, lobby):
		communication.ServerSocket.__init__(self, port, ClientConnection, (lobby,))
		
		
class ClientConnection(threading.Thread):
	"""
	A connection from a client.
	"""
	_alive = True #: False when this thread is ready to be killed.
	_client_socket = None #: The connection to the client.
	_data_box = None #: A container for data remaining to be processed.
	_lobby = None #: The lobby with which the client interacts.
	_lock = None #: A lock to keep the event queue thread-safe.
	_queue = None #: A list of messages to send to the player.
	_player = None #: The _Player object of the player this thread manages.
	_player_name = None #: The name of the player this connection handles.
	
	def __init__(self, client, args):
		"""
		Performs a handshake with the would-be client before accepting the
		connection and exposing it to the server.
		
		@type client: C{socket.socket}
		@param client: The socket by which connection with the client is made
		    possible.
		@type args: sequence
		@param args: The lobby server with which this client will interact.
		"""
		threading.Thread.__init__(self)
		self.setDaemon(False)
		
		(lobby,) = args
		
		self._lock = threading.RLock()
		self._queue = []
		self._lobby = lobby
		self._data_box = data_box.DataBox()
		self._client_socket = client_socket = communication.ClientSocket(client, self._data_box)
		
		try:
			self._client_socket.readData(True)
		except: #No harm in failing at this point.
			self.kill()
			return
		data = self._data_box.getLine()
		try:
			match = re.match(r"(\w+) (\d+\.\d+(?:\.\d+)?[a-z]?)", data)
			if not match:
				self._client_socket.sendData("FAIL invalid identifier")
				self.kill()
			elif not match.group(2) == _VERSION:
				self._client_socket.sendData("FAIL server is running version %s" % _VERSION)
				self.kill()
			elif not lobby.isAlive():
				self._client_socket.sendData("FAIL server has shut down")
				self.kill()
			else:
				player_name = match.group(1)
				error = lobby.validatePlayer(player_name)
				if error:
					self._client_socket.sendData("FAIL %s" % (error))
					self.kill()
				else:
					self._player = lobby.addPlayer(player_name)
					self._player_name = player_name
					
					self._client_socket.sendData("PASS %s %i" % (player_name, self._player.getTeamID() + 1))
					for description in [other_player.describe() for other_player in self._lobby.getPlayers() if not other_player == self._player]:
						self._client_socket.sendData(description)
					self._client_socket.sendData("LEND")
					
					lobby.connectPlayer(self._player, self)
		except Exception, e:
			print e
			lobby.disconnectPlayer(self._player_name)
			
	def kill(self):
		"""
		Indicates that this connection should terminate gracefully.
		"""
		self._alive = False
		
	def pushEvent(self, event):
		"""
		Queues an event for delivery to the client.
		
		@type event: basestring
		@param event: A non-newline-terminated protocol event.
		"""
		self._lock.acquire()
		self._queue.append(event)
		self._lock.release()
		
	def run(self):
		"""
		Continually reads and processes data from the client, and delivers any
		queued data.
		"""
		readData = self._client_socket.readData
		sendData = self._client_socket.sendData
		data_box = self._data_box
		lock = self._lock
		queue = self._queue
		lobby = self._lobby
		player_name = self._player_name
		try:
			while self._alive:
				readData()
				while data_box.hasLines():
					lobby.processLine(data_box.getLine(), player_name)
					
				lock.acquire()
				if queue:
					sendData('\n'.join(queue))
					self._queue = queue = []
				lock.release()
				
				time.sleep(0.25)
		except:
			pass
		lobby.disconnectPlayer(player_name)
		self._client_socket.kill()
		
		
class _Player(object):
	"""
	A container that locally represents a player connected to a lobby server.
	"""
	_name = None #: The player's name.
	_team_id = None #: The ID of the team to which this player belongs.
	
	def __init__(self, name, team_id):
		"""
		Initialises the state of this player, setting the name and team to which
		it belongs.
		
		@type name: basestring
		@param name: The name of the player being represented.
		@type team_id: int
		@param team_id: The ID of the team to which this player initially belongs.
		"""
		self._name = name
		self._team_id = team_id
		
	def describe(self):
		"""
		Returns a protocol-event description of this player.
		
		@rtype: str
		@return: A protocol-event description of this player.
		"""
		return "PLYR %s %i" % (self._name, self._team_id + 1)
		
	def getName(self):
		"""
		Returns the name of this player.
		
		@rtype: str
		@return: The name of this player.
		"""
		return self._name
		
	def getTeamID(self):
		"""
		Returns the team ID of this player.
		
		@rtype: int
		@return: This player's team ID.
		"""
		return self._team_id
		
	def setTeamID(self, team_id):
		"""
		Assigns a new team ID to this player.
		
		@type team_id: int
		@param team_id: The team ID to assign to this player.
		"""
		self._team_id = team_id
		
		
class _Team(object):
	"""
	A container that locally represents the membership of a team.
	"""
	_lock = None #: A lock used for thread control.
	_members = None #: The players who comprise this team.
	
	def __init__(self):
		"""
		Initialises this team.
		"""
		self._members = []
		self._lock = threading.Lock()
		
	def addMember(self, player):
		"""
		Adds a player to this team.
		
		@type player: L{_Player}
		@param player: The player to be added.
		"""
		self._lock.acquire()
		if not player in self._members:
			self._members.append(player)
		self._lock.release()
		
	def getMembers(self):
		"""
		Returns a list of all players that comprise this team.
		
		@rtype: list
		@return: A list of all L{_Player}s that comprise this team.
		"""
		self._lock.acquire()
		try:
			return self._members[:]
		finally:
			self._lock.release()
			
	def removeMember(self, player):
		"""
		Removes a player from this team.
		
		@type player: L{_Player}
		@param player: The player to be removed.
		"""
		self._lock.acquire()
		if player in self._members:
			self._members.remove(player)
		self._lock.release()
		